package com.itmck.lock;

import lombok.extern.slf4j.Slf4j;

import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;


/**
 * 基于Redis的分布式锁
 *
 * @author luozhan
 * @date 2020-07
 */
@Slf4j
public class RedisLock implements DistributedLock {

    public static final int DEFAULT_WAIT_TIME = 5;
    public static final int DEFAULT_LEASE_TIME = 120;
    private static final String LOCK_PREFIX = "LOCK.";
    private static final int INFINITY = 999_999_999;
    private static final String APP_ID = UUID.randomUUID().toString();

    private IRedisClient cacheRedisClient;

    private String lockKey;
    private String lockValue;

    public RedisLock(String key, IRedisClient cacheRedisClient) {
        this.lockKey = LOCK_PREFIX + key;
        // 应用ID+线程ID，保证每个线程都拥有唯一的value，防止锁误删
        this.lockValue = APP_ID + ":" + Thread.currentThread().getId();
        this.cacheRedisClient = cacheRedisClient;
    }

    public String getLockKey() {
        return this.lockKey;
    }

    /**
     * 加锁
     * 无限等待，直到获取锁成功或抛出异常
     */
    @Override
    public void lock() {
        tryLock(INFINITY, DEFAULT_LEASE_TIME, TimeUnit.SECONDS);
    }

    /**
     * 加锁
     * 无限等待，直到获取锁成功或抛出异常
     *
     * @param leaseTime 锁的最大期限（小于等于0 则使用默认期限）
     * @param unit      时间单位
     */
    @Override
    public void lock(long leaseTime, TimeUnit unit) {
        tryLock(INFINITY, leaseTime, unit);
    }

    /**
     * 尝试加锁
     * 只会尝试一次，立即返回结果
     *
     * @return true获取到锁，false获取失败
     */
    @Override
    public boolean tryLock() {
        return tryLock(0, DEFAULT_LEASE_TIME, TimeUnit.SECONDS);
    }

    /**
     * 尝试加锁（指定最长等待时间）
     *
     * @param waitTime 等待时间，超过后返回false
     * @param unit     时间单位
     * @return 成功返回true，超时返回false
     */
    @Override
    public boolean tryLock(long waitTime,TimeUnit unit) {
        return tryLock(waitTime, DEFAULT_LEASE_TIME, unit);
    }

    /**
     * 尝试加锁
     * <p>
     * 示例：
     * // 最多等待100秒，上锁以后10秒自动解锁
     * lock.tryLock(100, 10, TimeUnit.SECONDS);
     *
     * @param waitTime  等待时间 (小于0 则使用默认等待时间；等于0 则只尝试一次加锁)
     * @param leaseTime 锁的最大期限（小于等于0 则使用默认期限）
     * @param unit      时间单位
     * @return 成功返回true，超时返回false
     */
    @Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
        waitTime = unit.toMillis(waitTime < 0 ? DEFAULT_WAIT_TIME : waitTime);
        // 必须设置锁的自动失效时间，防止死锁
        leaseTime = leaseTime <= 0 ? TimeUnit.SECONDS.convert(DEFAULT_LEASE_TIME, unit) : leaseTime;
        long startTime = System.currentTimeMillis();
        int tryCount = 0;
        while (true) {
            // 加锁操作必须是原子的
            Boolean result = cacheRedisClient.setNxExAtomic(lockKey, lockValue, (int) leaseTime);

            if (result != null && result) {
                log.debug("加锁({})成功！，最大占用锁的时间为{}毫秒", lockKey, unit.toMillis(leaseTime));
                return true;
            }
            long passTime = System.currentTimeMillis() - startTime;
            log.debug("尝试加锁第({})次失败！继续尝试{}毫秒,", ++tryCount, waitTime - passTime);
            if (waitTime <= passTime) {
                return false;
            }
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    @Override
    public void unlock() {
        cacheRedisClient.del(lockKey);

        // 解锁操作须保证原子性，使用lua脚本判断一致性和删除key
        /*
        String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "return redis.call('del', KEYS[1]) " +
                "else " +
                "return 0 " +
                "end";
        RedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);
        */


        /*
        Long result = redisTemplate.execute(script, Collections.singletonList(lockKey), lockValue);
        if (result != null && result == 0) {
            log.warn("线程{}解锁({})失败，可能锁已经到期自动释放", Thread.currentThread().getId(), lockKey);
        } else {
            log.debug("线程{}解锁({})成功", Thread.currentThread().getId(), lockKey);
        }
        */
    }


    @Override
    public Condition newCondition() throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    @Override
    public void lockInterruptibly() throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }
}
